package gui;

import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import javax.swing.JLayeredPane;

import gui.event.*;

/**
 *	@(#)DraggableLabel.java
 *
 *	<p>The <code>DraggableLabel</code> class extends the <code>GameLabel</code> class to
 *	add drag and drop support. A dragged label will be always the topmost one after the
 *	dragging procedure.</p>
 *	<p>You can define a polygonal active area if the underlying icon is not rectangular.
 *	In this case the label won't display tooltips or process events if the event occured
 *	without the polygon.</p>
 *	<p>Also, draggable labels can be attached to each other by dropping the center of
 *	mass of one label onto another one. To enable this feature you have to give your
 *	label an attachmentID and provide this to the labels which are designed to be the
 *	drop targets. Also, you can assign attachment listeners which will be notified by
 *	any attachments done.</p>
 *
 *	@author Grzegorz Kobiela
 *	@version 1.5 2008/4/21
 */
public class DraggableLabel
extends GameLabel
implements Polygonal
{
	/** The active area of the component. */
	protected Polygon polygon;

	/** The list of attachment listeners. */
	protected ArrayList<AttachmentListener> attachmentListeners;

	/** The list of attached draggable labels. */
	protected ArrayList<DraggableLabel> attachments;

	/** The list of accepted attachment ids. */
	protected ArrayList<Object> acceptedIDs;

	/** The draggable label this one is attached to. */
	protected DraggableLabel attachedTo;

	/** The attachment id (null means not attachable). */
	protected Object attachmentID;

	/** The original layer before dragging. */
	protected int originalLayer;

	/** The original location before dragging. */
	protected Point offsetPoint;

	/** The ability to be dragged. */
	protected boolean draggable;

	/**
	 *	Creates a new <code>DraggableLabel</code> instance with the given icon
	 *	and alpha value that has to be applied to the icon.
	 *	@param icon the icon
	 *	@param alpha the alpha value
	 */
	public DraggableLabel(javax.swing.ImageIcon icon, float alpha) {
		super(icon, alpha);
		polygon = new Polygon(new int[]{0, getWidth(), getWidth(), 0}, new int[]{0, 0, getHeight(), getHeight()}, 4);
		attachmentListeners = new ArrayList<AttachmentListener>();
		attachments = new ArrayList<DraggableLabel>();
		acceptedIDs = new ArrayList<Object>();
		attachedTo = null;
		attachmentID = null;
		offsetPoint = null;
		draggable = true;
		addMouseListener(
			new MouseAdapter() {
				/** Starts the dragging process. */
				public void mousePressed(MouseEvent e) {
					if(!draggable || !isEnabled() || e.getButton() != MouseEvent.BUTTON1 || !(getParent() instanceof JLayeredPane)) return;
					drag(e);
				}

				/** Stops the dragging process. */
				public void mouseReleased(MouseEvent e) {
					if(!draggable || !isEnabled() || e.getButton() != MouseEvent.BUTTON1 || !(getParent() instanceof JLayeredPane)) return;
					Point location = offsetPoint;
					drop(e);
					if(attachmentID == null) return;

					/* Try to attach the label */
					JLayeredPane parent = (JLayeredPane)getParent();
					for(Component component: parent.getComponents()) {
						if(!(component instanceof DraggableLabel)) continue;
						DraggableLabel label = (DraggableLabel)component;
						if(!label.acceptedIDs.contains(attachmentID)) continue;
						if(parent.getLayer(label) > parent.getLayer(DraggableLabel.this)) continue;
						Point centerOfMass = getCenterOfMass();
						centerOfMass.translate(getX() - label.getX(), getY() - label.getY());
						if(!label.contains(centerOfMass)) continue;
						if(attachedTo == label) return;
						label.attach(DraggableLabel.this);
						return;
					}

					/* Detach the label */
					detach(location);
				}
			}
		);
		addMouseMotionListener(
			new MouseMotionAdapter() {
				public void mouseDragged(MouseEvent e) {
					if(offsetPoint != null) setLocation(getX() + (e.getX() - offsetPoint.x), getY() + (e.getY() - offsetPoint.y));
				}
			}
		);
	}

	/**
	 *	Creates a new <code>DraggableLabel</code> instance with the given icon
	 *	and a default alpha value.
	 *	@param icon the icon
	 */
	public DraggableLabel(javax.swing.ImageIcon icon) {
		this(icon, 1.0f);
	}

	/**
	 *	Inititates the dragging process.
	 *	@param e the mouse event
	 */
	protected synchronized void drag(MouseEvent e) {
		for(int i = attachments.size() - 1; i >= 0; i--) attachments.get(i).drag(e);
		JLayeredPane parent = (JLayeredPane)getParent();
		originalLayer = parent.getLayer(DraggableLabel.this);
		parent.setLayer(DraggableLabel.this, JLayeredPane.DRAG_LAYER);
		offsetPoint = new Point(e.getPoint());
	}

	/**
	 *	Finalizes the dragging process.
	 *	@param e the mouse event
	 */
	protected void drop(MouseEvent e) {
		for(DraggableLabel label: attachments) label.drop(e);
		JLayeredPane parent = (JLayeredPane)getParent();
		parent.setLayer(DraggableLabel.this, originalLayer);
		parent.moveToFront(DraggableLabel.this);
		int xMax = parent.getWidth() - getWidth();
		int yMax = parent.getHeight() - getHeight();
		if(getX() < 0) setLocation(0, getY());
		if(getX() > xMax) setLocation(xMax, getY());
		if(getY() < 0) setLocation(getX(), 0);
		if(getY() > yMax) setLocation(getX(), yMax);
		offsetPoint = null;
	}

	/**
	 *	Moves the label and all its attachments to the specified location.
	 *	@param x the x-coordinate of the new location
	 *	@param y the y-coordinate of the new location
	 */
	@Override
	public void setLocation(int x, int y) {
		for(DraggableLabel label: attachments) label.setLocation(label.getX() + x - getX(), label.getY() + y - getY());
		super.setLocation(x, y);
	}

	/**
	 *	Moves all attachments to their new positions after a rotation.
	 *	@param bounds the bounds before the rotation
	 *	@param angle the angle
	 */
	public void rearrange(Rectangle bounds, int angle) {
		if(attachments.size() == 0) return;
		int dx, dy, nx, ny, x, y;
		int x0 = bounds.x + bounds.width / 2;
		int y0 = bounds.y + bounds.height / 2;
		double phi = Math.toRadians(angle);
		double c = Math.cos(phi);
		double s = Math.sin(phi);
		for(DraggableLabel label: attachments) {
			x = label.getX() + (dx = label.getWidth() / 2 - x0);
			y = label.getY() + (dy = label.getHeight() / 2 - y0);
			nx = (int)Math.round(c * x - s * y) - dx;
			ny = (int)Math.round(s * x + c * y) - dy;
			label.setLocation(nx, ny);
		}
	}

	/**
	 *	Checks if the given point is located within the shape of the label.
	 *	@param x the x-coordinate of the point
	 *	@param y the y-coordinate of the point
	 */
	@Override
	public boolean contains(int x, int y) {
		return polygon.contains(x, y);
	}

	/**
	 *	Checks if the given point is located within the shape of the label.
	 *	@param point the point
	 */
	@Override
	public boolean contains(Point point) {
		return contains(point.x, point.y);
	}

	/**
	 *	Adds the given attachment listener.
	 *	@param listener the listener
	 */
	public synchronized void addAttachmentListener(AttachmentListener listener) {
		attachmentListeners.add(listener);
	}

	/**
	 *	Removes the given attachment listener.
	 *	@param listener the listener
	 */
	public synchronized void removeAttachmentListener(AttachmentListener listener) {
		attachmentListeners.remove(listener);
	}

	/**
	 *	Attaches the given label at the given location.
	 *	@param label the label
	 *	@param location the location
	 */
	public void attach(DraggableLabel label, Point location) {
		if(!acceptedIDs.contains(label.attachmentID)) return;
		if(label.getLocation() != location) label.setLocation(location);
		attachments.add(label);
		if(label.attachedTo != null) label.detach();
		label.attachedTo = this;
		fireAttachmentEvent(label, location);
		label.fireAttachmentEvent(this, location);
	}

	/**
	 *	Attaches the given label.
	 *	@param label the label
	 */
	public void attach(DraggableLabel label) {
		attach(label, label.getLocation());
	}

	/**
	 *	Detaches the label at the given location.
	 *	@param location the location
	 */
	public void detach(Point location) {
		if(attachedTo == null) return;
		attachedTo.attachments.remove(this);
		fireDetachmentEvent(attachedTo, location);
		attachedTo.fireDetachmentEvent(this, location);
		attachedTo = null;
	}

	/**
	 *	Detaches the label at the current location.
	 */
	public void detach() {
		detach(getLocation());
	}

	/**
	 *	Fires an attachment event after the succesful attachment
	 *	of the given source component at the given location.
	 *	@param source the source
	 *	@param location the location
	 */
	protected synchronized void fireAttachmentEvent(Object source, Point location) {
		AttachmentEvent event = new AttachmentEvent(source, location);
		for(AttachmentListener listener: attachmentListeners) listener.componentAttached(event);
	}

	/**
	 *	Fires an attachment event after the succesful detachment
	 *	of the given source component at the given location.
	 *	@param source the source
	 *	@param location the location
	 */
	protected synchronized void fireDetachmentEvent(Object source, Point location) {
		AttachmentEvent event = new AttachmentEvent(source, location);
		for(AttachmentListener listener: attachmentListeners) listener.componentDetached(event);
	}

	/**
	 *	Adds the given attachment id to the list of accepted ones.
	 *	@param id the attachment id
	 */
	public synchronized void addAcceptedID(Object id) {
		acceptedIDs.add(id);
	}

	/**
	 *	Removes the given attachment id from the list of accepted ones.
	 *	@param id the attachment id
	 */
	public synchronized void removeAcceptedID(Object id) {
		acceptedIDs.remove(id);
	}

	/**
	 *	Sets the attachment id to the given value.
	 *	@param id the attachment id
	 */
	public synchronized void setAttachmentID(Object id) {
		attachmentID = id;
	}

	/**
	 *	Sets the ability to be draggable to the given value
	 *	@param draggable provide true if the label is draggable
	 */
	public void setDraggable(boolean draggable) {
		this.draggable = draggable;
	}

	/**
	 *	Returns true if the label is draggable.
	 *	@return true if the label is draggable
	 */
	public boolean isDraggable() {
		return draggable;
	}

	/**
	 *	Sets the outline polygon defining the active area.
	 *	Throws a null pointer exception if null is provided.
	 *	@param polygon the polygon
	 */
	public void setPolygon(Polygon polygon) {
		if(polygon != null) {
			this.polygon = new Polygon(polygon.xpoints, polygon.ypoints, polygon.npoints);
		} else {
			throw new NullPointerException("The outline polygon must not be null in DraggableLabel.setPolygon(Polygon)!");
		}
	}

	/**
	 *	Returns the outline polygon.
	 *	@return the outline polygon
	 */
	public Polygon getPolygon() {
		return new Polygon(polygon.xpoints, polygon.ypoints, polygon.npoints);
	}

	/**
	 *	Returns the center of mass.
	 *	@return the center of mass
	 */
	public Point getCenterOfMass() {
		Point p = new Point();
		int n = polygon.npoints;
		for(int i = 0; i < n; i++) p.translate(polygon.xpoints[i], polygon.ypoints[i]);
		return new Point(p.x / n, p.y / n);
	}
}